Skip to content

feat: universal element style system — resolver, schema expansion, live preview#394

Open
ChuckBuilds wants to merge 6 commits into
feat/adaptive-layoutfrom
feat/element-style
Open

feat: universal element style system — resolver, schema expansion, live preview#394
ChuckBuilds wants to merge 6 commits into
feat/adaptive-layoutfrom
feat/element-style

Conversation

@ChuckBuilds

Copy link
Copy Markdown
Owner

Summary

Stacked on #393. Universal per-element customization (font / size / color / x-y offsets) for every plugin without duplicated code, plus a live preview on the config page.

  • src/element_style.pyElementStyleResolver: one shared implementation of font loading (superset of the four diverged per-plugin loaders), customization.layout offset reads, and the user-override check. The override check is the load-bearing piece: the save flow bakes full schema defaults into every saved config, so key presence never means user intent — the resolver compares against the plugin's own config_schema.json defaults. This retires the hand-maintained _CLASSIC_FONT_DEFAULTS dicts that shipped broken twice.
  • x-style-elements schema expansion: a plugin declares a styleable element in ~6 lines; SchemaManager.load_schema (and the config-page render, and defaults_from_schema_file) expand it into full font-selector/color-picker/offset config blocks. Idempotent, deepcopied, hand-written blocks always win. Colors follow the same provenance rule as fonts (the form always posts the RGB inputs), so untouched saves can't clobber semantic classic colors like a gold-on-touchdown score.
  • POST /api/v3/plugins/preview + Live Preview panel: renders the plugin headlessly (pure PIL, install_deps=False, skip_update default) with the current unsaved form values, at the real panel size or a chosen test size. Reuses the exact save-path form parser (extracted verbatim as parse_plugin_config_form) so preview and save can never disagree.

Verification

  • 100+ new tests (test_element_style.py, test_schema_style_expansion.py, test_plugin_preview.py); full suite green.
  • Verified end-to-end on the devpi (192x48): expanded schema in the real web UI, preview with unsaved red-title override, real form save, and the override rendering on the physical panel.
  • Two bugs found during hardware testing are fixed in this branch: the config-page render loads the schema from disk (bypassing the schema manager) and needed the same expansion; htmx caches hx-post paths so the preview size selector uses hx-vals.
  • Adopted by of-the-day v1.3.0 and consumed by football v2.8.2 / music v1.1.1 (monorepo PRs); all keep guarded fallbacks for older cores.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FqzC1nzTWL4kaqgMaQZFam

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 79fcb8b4-6e7a-48ca-8cb5-22d1f11bd12f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/element-style

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codacy-production

codacy-production Bot commented Jul 11, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 97 complexity · 0 duplication

Metric Results
Complexity 97
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@ChuckBuilds

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ChuckBuilds ChuckBuilds force-pushed the feat/element-style branch 3 times, most recently from 4c8150c to 61f3db3 Compare July 11, 2026 16:41
Chuck and others added 6 commits July 11, 2026 12:45
One shared implementation of the three things every customizable plugin
re-invented: a font loader, the customization.layout x/y-offset reader,
and the "did the user actually override this font?" check.

The override check is the load-bearing piece: the web UI's save flow
writes full schema defaults into config.json on every save, and the
plugin manager merges defaults again before instantiation, so key
presence never means user intent. The resolver compares against the
plugin's own schema defaults (via schema_manager), degrading to
caller-supplied classic defaults when unavailable. This retires the
hand-maintained _CLASSIC_FONT_DEFAULTS dicts that shipped broken twice.

The loader is a superset of the four per-plugin variants: alias
resolution (baseball), truetype for TTF/OTF/BDF (FreeType loads BDF at
native size), .pil sidecar fallback for BDF (football), fallback font,
PIL default — never raises. Also introduces the text_color convention
([r,g,b], absent = keep the plugin's hardcoded color).

BasePlugin gains a lazy style_resolver property (invalidated on config
change) and element_style() sugar; standalone helpers receive the
resolver from their owning plugin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FqzC1nzTWL4kaqgMaQZFam
…ance

Plugins can now declare styleable display elements once, compactly, on
their customization schema ("x-style-elements") instead of hand-copying
the ~50-line font/font_size/text_color/offset property blocks (currently
duplicated 52x across the plugin monorepo). SchemaManager.load_schema
expands declarations before caching, so the config form, save path,
validation, and defaults generation all see the same shape; the single
expansion implementation lives in src.element_style and is also applied
by defaults_from_schema_file, keeping the web UI's view and a plugin's
raw-schema-file view of the defaults provably identical (parity test).

Generated blocks use only widgets the config form already renders
(font-selector, color-picker, number inputs) and update x-propertyOrder
when present (the template only renders listed keys). Expansion is
idempotent, never mutates its input or the cached/on-disk schema, and a
hand-written block for the same element always wins.

Color gets the same provenance rule as fonts: the web form always posts
the RGB inputs, so a saved config carries the schema-default color
whether or not the user touched it — the resolver now only honors a
color that DIFFERS from the schema default, and keeps the plugin's
classic (possibly state-dependent, e.g. gold-on-touchdown) color
otherwise.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FqzC1nzTWL4kaqgMaQZFam
Adds POST /api/v3/plugins/preview: renders a plugin headlessly with the
CANDIDATE (unsaved) config and returns a PNG — so users can see exactly
what the panel will show before saving, at their real panel size (from
display.hardware) or a chosen test size.

Two extractions make it drift-proof rather than parallel-implemented:
- dev_server's _render_once moves to
  src/plugin_system/testing/render_service.py (pure PIL via
  VisualTestDisplayManager, install_deps=False always — safe in the web
  process, which never touches display hardware); the dev server now
  wraps it.
- save_plugin_config's ~350-line form->config conversion is extracted
  verbatim as parse_plugin_config_form and shared by the preview
  endpoint, so preview and save can never interpret the form
  differently.

update() is skipped by default (no network on the request thread);
plugins with a test/harness.json get their mock-data fixture primed
instead, and ?skip_update=0 opts into a live update. The config page
gains a Live Preview panel (HTMX hx-include of the existing form, size
selector, pixelated img fragment) — works for every plugin with zero
per-plugin code, including the x-style-elements font/size/color fields.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FqzC1nzTWL4kaqgMaQZFam
pages_v3's plugin-config partial loads config_schema.json directly from
disk rather than through SchemaManager.load_schema, so declared style
elements expanded everywhere EXCEPT the form the user actually sees.
Found live on the devpi: the API served the expanded schema and the
save path validated against it, but the config page rendered no
font/color/offset fields. Apply the same (idempotent) expansion here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FqzC1nzTWL4kaqgMaQZFam
Found live on the devpi: selecting a preview size did nothing because
htmx snapshots the request path when it processes the button, so the
size dropdown's onchange mutation of hx-post never took effect. The size
now travels as a __preview_size=WxH form field attached via hx-vals
(evaluated at request time); the endpoint honors it (query args still
win for API callers) and strips it before form parsing so it can't leak
into the candidate config.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FqzC1nzTWL4kaqgMaQZFam
Same defense as the dev server's find_plugin_dir (and pages_v3's
existing pattern): plugin_id arrives in request input and is used to
build filesystem paths — reject anything outside ^[a-zA-Z0-9_-]{1,64}$
before touching the filesystem.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FqzC1nzTWL4kaqgMaQZFam
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant